/* Copyright (c) 2011-2013 ISP RAS, Moscow. For more information please write to

      mermakov@ispras.ru
      svartanov@ispras.ru

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package ru.ispras.coffeemachine.instrument;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.bcel.classfile.Utility;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.INVOKESTATIC;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.MethodGen;

import ru.ispras.coffeemachine.target.Concurrency.Static;
import ru.ispras.coffeemachine.target.Type;

/**
 * Instruction list generator for method invocation instructions
 *
 * @author Sergey Vartanov (me@enzet.ru)
 * @version 1.2.0
 */
public class Invoker
{

	private static CoffeeInstructionFactory factory;
	private static ConstantPoolGen cpg;
	private static MethodGen method;

	/**
	 * Utility class for TSan interceptors implementation
	 *
	 * @author Mikhail Ermakov
	 */
	public static class Interceptor
	{
		protected final String[] interceptorSignature;

		protected final String className;
		protected final String method;
		protected final String signature;

		protected final boolean isStatic;

		public Interceptor(String className, String method, String signature, boolean isStatic, String[] interceptorSignature)
		{
			this.className = className;
			this.method = method;
			this.signature = signature;
			this.isStatic = isStatic;
			this.interceptorSignature = interceptorSignature;
		}

		public boolean isStatic()
		{
			return isStatic;
		}

		public String getSignature()
		{
			return signature.replaceAll("\\.\\*\\?", "java/lang/Object");
		}

		public String getFullSignature()
		{
			String result = className + method + "(";
			String []params = Utility.methodSignatureArgumentTypes(signature);
			for (String param : params)
			{
				result += param;
			}
			result += ")" + Utility.methodSignatureReturnType(signature);

			return result;
		}

		public boolean matches(String className, String method, String signature)
		{
			if (!this.className.equals(className))
			{
				return false;
			}
			String targetSignature = method + signature.replaceAll("\\).*", ")");
			String insertionSignature = this.method + this.signature.replaceAll("\\).*", ")");
			if (targetSignature.equals(insertionSignature))
			{
				return true;
			}
			else if (targetSignature.replaceAll("L.*?;", "Ljava/lang/Object;").equals(insertionSignature))
			{
				return true;
			}
			return false;
		}

		public List<Type> getParamTypes()
		{
			List<Type> types = Type.parseSignature(signature);

			if (!isStatic)
			{
				types.add(0, Type.R);
			}
			return types;
		}
	}

	private static class BeforeInterceptor extends Interceptor {

		public BeforeInterceptor(String className, String methodName, String methodSignature, boolean isStatic, String[] interceptorSignature) {

			super(className, methodName, methodSignature, isStatic, interceptorSignature);
		}
	}

	private static class AfterInterceptor extends Interceptor {

		private String returnType;

		public AfterInterceptor(String className, String methodName, String methodSignature, boolean isStatic, String[] interceptorSignature) {

			super(className, methodName, methodSignature, isStatic, interceptorSignature);

			returnType = methodSignature.substring(methodSignature.indexOf(')') + 1);
			if (returnType.equals("V"))
			{
				returnType = null;
			}
		}
	}

	private static class ExceptionInterceptor extends Interceptor {

		public ExceptionInterceptor(String className, String methodName, String methodSignature, boolean isStatic, String[] interceptorSignature) {

			super(className, methodName, methodSignature, isStatic, interceptorSignature);
		}
	}

	private static List<Interceptor> interceptors = new ArrayList<Interceptor>();
	private static Set<Interceptor> coveredInterceptors = new HashSet<Interceptor>();

	/**
	 * Read interceptors from class.
	 */
	public static void init(Class<?> clas)
	{
		Method[] methods = clas.getMethods();

		for (Method method : methods)
		{
			String name = method.getName();

			if (name.charAt(1) == '_')
			{
				String interceptorSignature[] = new String[3];

				interceptorSignature[0] = clas.getCanonicalName();
				interceptorSignature[1] = name;
				interceptorSignature[2] = "(";

				String c = name.substring(2, name.lastIndexOf('_')).replaceAll("_", "/");
				String m = name.substring(name.lastIndexOf('_') + 1);
				String s = "(";

				Class<?> [] paramTypes = method.getParameterTypes();

				int ret = (method.getReturnType().getName().equals("void")) ? 0 : 1;

				boolean isStatic = (method.getAnnotation(Static.class) != null);

				for (int i = isStatic ? 0 : 1; i < paramTypes.length - ret; i ++)
				{
					s += toBytecodeType(method.getParameterTypes()[i].toString());
				}
				for (int i = 0; i < paramTypes.length; i ++)
				{
					interceptorSignature[2] += toBytecodeType(method.getParameterTypes()[i].toString());
				}
				s += ")" + toBytecodeType(method.getReturnType().toString());
				interceptorSignature[2] += ")" + toBytecodeType(method.getReturnType().toString());

				switch (name.charAt(0))
				{
					case 'a': interceptors.add(new AfterInterceptor(c, m, s, isStatic, interceptorSignature)); break;
					case 'b': interceptors.add(new BeforeInterceptor(c, m, s, isStatic, interceptorSignature)); break;
					case 'e': interceptors.add(new ExceptionInterceptor(c, m, s, isStatic, interceptorSignature)); break;
					default: continue;
				}
			}
		}
	}

	private static String toBytecodeType(String stringType)
	{
		String s = "";

		Type t;
		t = Type.fromVerboseString(stringType);

		if (t.isScalar()) return t.toString();

		if (stringType.equals("void"))
		{
			return "V";
		}
		else
		{
			s = stringType.replaceAll("class ", "").replaceAll("interface ", "");

			if (s.charAt(0) != '[')
			{
				s = "L" + s + ";";
			}
		}
		return s.replaceAll("\\.", "/");
	}

	/**
	 * Methods from the file with following format:
	 * <tt>[b|a|2|e] method_name</tt>
	 * <p>
	 * <tt>b</tt> for "before", <tt>a</tt> for "after", <tt>2</tt> for both
	 * "before" and "after", <tt>e</tt> for exception, <tt>method_name</tt> is
	 * class name, point, method name, method signature.
	 */
	public static void setClassContext(ConstantPoolGen cpg, CoffeeInstructionFactory factory)
	{
		Invoker.cpg = cpg;
		Invoker.factory = factory;
	}

	public static void setMethod(MethodGen method)
	{
		Invoker.method = method;
	}

	/**
	 * Creates instruction list for before TSan interceptors.
	 *
	 * @param className invocation class name
	 * @param methodName invocation method name
	 * @param methodSignature invocation method signature
	 * @return instruction list if interceptors cover specified method, empty
	 *         list otherwise
	 */
	public static InstructionList createBeforeList(String className, String methodName, String methodSignature)
	{
		InstructionList instructionList = new InstructionList();

		boolean isBefore = false;
		boolean isAfter = false;;
		Interceptor targetInterceptor = null;
		for (Interceptor interceptor : interceptors)
		{
			if (interceptor.matches(className, methodName, methodSignature)) {
				coveredInterceptors.add(interceptor);
				if ((interceptor instanceof AfterInterceptor))
				{
					if (targetInterceptor == null)
					{
						targetInterceptor = interceptor;
					}
					isAfter = true;
				}
				else if (interceptor instanceof BeforeInterceptor)
				{
					targetInterceptor = interceptor;
					isBefore = true;
				}
			}
		}

		if (targetInterceptor != null)
		{
			List<Type> types = targetInterceptor.getParamTypes();

			int length = types.size();

			if (types.size() > 0)
			{
				// 1. Duplicate parameters in stack

				if (isAfter && isBefore)
				{
					int[] indexes = new int[length * 2];
					for (int i = 0; i < length; i++)
					{
						indexes[i] = i;
						indexes[i + length] = i;
					}
					instructionList.append(factory.getElements(types.toArray(new Type[0]), indexes, method.getMaxLocals()));
				}
				else
				{
					instructionList.append(factory.getElements(types.toArray(new Type[0]), method.getMaxLocals()));
				}
			}
		}

		if (isBefore)
		{
			try
			{
				instructionList.append(createMethod(targetInterceptor.interceptorSignature));
			}
			catch (Exception e)
			{
				System.out.println(targetInterceptor.signature);
				e.printStackTrace();
			}
		}
		return instructionList;
	}

	/**
	 * Creates instruction list for before TSan interceptors.
	 *
	 * @param className invocation class name
	 * @param methodName invocation method name
	 * @param methodSignature invocation method signature
	 * @return instruction list if interceptors cover specified method, empty
	 *         list otherwise
	 */
	public static InstructionList createAfterList(String className, String methodName, String methodSignature)
	{
		InstructionList instructionList = new InstructionList();

		for (Interceptor interceptor : interceptors)
		{
			if (!(interceptor instanceof AfterInterceptor))
			{
				continue;
			}
			if (interceptor.matches(className, methodName, methodSignature)) {

				coveredInterceptors.add(interceptor);
				instructionList.append(createMethod(interceptor.interceptorSignature));
				break;
			}
		}
		return instructionList;
	}

	public static boolean isBefore(String className, String methodName, String methodSignature)
	{
		for (Interceptor interceptor : interceptors)
		{
			if (interceptor.matches(className, methodName, methodSignature))
			{
				if (interceptor instanceof BeforeInterceptor)
				{
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Create invocation of method from class with interceptors (e.g.
	 * <code>Daemon</code> or <code>Concurrency</code>)
	 */
	protected static Instruction createMethod(String[] methodRef)
	{
		return new INVOKESTATIC(cpg.addMethodref(methodRef[0], methodRef[1], methodRef[2]));
	}

	/**
	 * Print all specified concurrency methods beyond coverage
	 */
	public static void printMethodsCoverage()
	{
		boolean hasFullCoverage = true;

		for (Interceptor interceptor : interceptors)
		{
			if (!coveredInterceptors.contains(interceptor))
			{
				if (hasFullCoverage)
				{
					System.out.println("Concurrency methods beyond coverage:");
					hasFullCoverage = false;
				}
				System.out.println(interceptor.getFullSignature());
			}
		}
		if (hasFullCoverage)
		{
			System.out.println("All methods under coverage.");
		}
	}
}
